(ns #^{:doc "Functions to retrieve routing information for
            specimen/couriers."}
  clj.jtacck.db.routing
  (:use [clj.jtacck.db.core :only [get-all-shipping-info]]
	[clojure.contrib.duck-streams :only [reader]]
	[ring.util.codec :only [url-encode]]
	[clojure.contrib.json :only [read-json]])
  (:require [clj.jtacck.cache :as cache]
	    [clj.jtacck.logging :as log]))

(def eta-cache-lifetime (* 20 60 1000)) ; 20 minutes

(defn make-directions-api-url
  "Takes an origin and destination and returns a Google API url to
  retrieve JSON describing the directions from the origin to the
  destination along the given sequence of waypoints (which may be
  empty).  Can optionally take a series of altering keywords and
  values to pass as parameters in the url. The sensor param is always
  false.

  See
   http://code.google.com/apis/maps/documentation/directions/#RequestParameters
  for more information on the possible parameters.

  The resulting url will be of the form:
   https://maps.googleapis.com/maps/api/directions/json?origin=<origin>&destination=<destination>&waypoints=<waypoints>&sensor=false&<otherparams>"
  [origin destination waypoints & opts]
  (let [waypoints-arry (apply str (interpose "|" waypoints))
	full-opts (list* :origin origin
			 :destination destination
			 :waypoints waypoints-arry
			 :sensor false
			 opts)
	params (map (fn [[param value]]
		      (str (name param) "=" (url-encode (str value)) "&"))
		    (partition 2 full-opts))]
    (apply str
	   "https://maps.googleapis.com/maps/api/directions/json?" params)))

(def #^{:doc "Using the same interface as make-directions-api-url,
             returns the map parsed from actually contacting the
             Google API to get a JSON object."}
     get-routing (comp read-json reader make-directions-api-url))

(defn get-total-duration
  "Given a directions API JSON map, gets the total duration, in
  seconds, for the travel."
  [json]
  (map
   (fn [route]
     (reduce +
	     (map
	      (fn [leg]
		(reduce +
			(map
			 (fn [step]
			   (get-in step [:duration :value]))
			 (:steps leg))))
	      (:legs route))))
   (:routes json)))

(defn get-courier-destination
  "Given a courier record map, returns their destination address."
  [courier]
  (get-in courier [:user :site :hospital :address]))

(defn get-courier-leftover-route
  "Returns a sequence of ClientSites that the given courier
  has left to reach.  This does not include their final destination."
  [courier]
  (drop-while (partial not= (:nextDest courier))
	      (:route courier)))

(defn get-courier-eta
  "Returns an estimated time of arrival for the given courier record
  map."
  [courier]
  (cache/cache-or-compute
   [::courier-eta courier] ; Courier should be diff if their loc changes
   (get-total-duration
    (get-routing		; from courier location to destination
     (str (:latitude courier) " " (:longitude courier))
     (get-courier-destination courier)
     (map (fn [site]			; Waypoints
	    (get-in site [:hospital :address]))
	  (get-courier-leftover-route courier))))
   eta-cache-lifetime))

(defn get-all-courier-etas
  "Returns a map from courier record maps to an estimated time of
  travel from their currently stored GPS location to their
  destination, in seconds.

  Note: This is cached with a lifetime of eta-cache-lifetime."
  []
  (let [shipping-info (get-all-shipping-info)
	couriers (into #{} (map :courier shipping-info))]
    (log/timing-info
     "get-all-courier-etas: "
     (cache/cache-or-compute
      ::courier-etas
      (into {} (map (fn [courier]
		      [courier (get-courier-eta courier)])
		    couriers))
      eta-cache-lifetime))))

(defn get-shipping-etas
  "Returns a map from shipping-info record map to estimated time of
  arrival to their respective destinations, in minutes."
  []
  (into {}
	(let [etas (get-all-courier-etas)]
	  (for [shipping-item (get-all-shipping-info)
		:let [courier-eta (first (get etas (:courier shipping-item)))]]
	    (let [eta (if courier-eta courier-eta -60)]
	      [shipping-item (Math/round (/ eta 60.0))]))))) ; In minutes

(defn get-item-etas
  "Like get-shipping-etas except expands each shipping info into a
  list of each item and returns a list of all item etas."
  []
  (for [[shipping-info eta] (get-shipping-etas)
	item (get-in shipping-info [:specimen :contents])]
    eta))